This page provides detailed technical documentation for future students, including implementation details, design patterns, debugging techniques, and common pitfalls.
SPI Protocol Implementation
SPI Mode 0 Configuration
The system uses SPI Mode 0 (CPOL=0, CPHA=0) for all SPI interfaces:
- CPOL=0: Clock idle state is LOW
- CPHA=0: Data is sampled on the rising edge of SCK, changed on the falling edge
- Bit Order: MSB first
- Clock Speed: 100kHz for Arduino→FPGA, configurable for FPGA→MCU
FPGA SPI Slave Implementation
Key Implementation Details:
CS-Based Protocol:
- Transaction begins when CS goes LOW
- Data is valid during CS LOW
- Transaction ends when CS goes HIGH
- CS must remain LOW for entire 16-byte packet transmission
- For SPI Mode 0, CS HIGH guarantees SCK is idle (CPOL=0)
Clock Edge Detection (from
arduino_spi_slave.sv):// Shift in data on SCK rising edge (CPHA=0) always_ff @(posedge sck) begin if (cs_n_prev_sck && !cs_n) begin // CS falling edge - new transaction, reset shift register packet_shift <= 128'd0; end else if (!cs_n) begin // CS low - shift in data (MSB first) packet_shift <= {packet_shift[126:0], sdi}; end // When CS is high, packet_shift retains value for CDC capture endPacket Framing:
- First byte is header (0xAA) for synchronization
- Fixed 16-byte packet size (128 bits total)
- Header validation ensures packet integrity
- Uses 128-bit shift register for efficient bit-level reception
Clock Domain Crossing (CDC):
- Data received in SCK domain (Arduino SCK, 100kHz)
- Must be transferred to FPGA system clock domain (3MHz)
- Strategy: CS-based safe read
- Wait for CS HIGH (transaction complete, SCK guaranteed idle)
- Wait 3 system clock cycles (1us) for settling
- Atomic read of all 16 bytes in one clock cycle
- Provides 10:1 timing margin (very safe)
MCU SPI Master Implementation
STM32L432KC SPI Configuration:
// SPI1 Configuration
// PB3 = SCK, PB5 = MOSI, PB4 = MISO, PA11 = NSS
SPI_InitTypeDef SPI_InitStruct;
SPI_InitStruct.Mode = SPI_MODE_MASTER;
SPI_InitStruct.Direction = SPI_DIRECTION_2LINES;
SPI_InitStruct.DataSize = SPI_DATASIZE_8BIT;
SPI_InitStruct.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0
SPI_InitStruct.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0
SPI_InitStruct.NSS = SPI_NSS_SOFT; // Software-controlled CS
SPI_InitStruct.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_32; // Adjust for desired speed
SPI_InitStruct.FirstBit = SPI_FIRSTBIT_MSB; // MSB first
SPI_InitStruct.TIMode = SPI_TIMODE_DISABLE;
SPI_InitStruct.CRCCalculation = SPI_CRCCALCULATION_DISABLE;Key Considerations: - NSS (CS) must be manually controlled in software mode (PA11) - Ensure proper timing between CS assertion and data transfer - Handle SPI busy flag correctly before starting new transaction - Use proper delay between transactions to allow FPGA to update data - Read 16 bytes in sequence, parsing header (0xAA) and data fields - FPGA operates in read-only mode (ignores MOSI, only shifts out on MISO)
FPGA Design Patterns
Dual SPI Slave Architecture
Pattern: Implementing multiple SPI slave interfaces on a single FPGA
Implementation: - Separate state machines for each SPI slave - Independent clock domains (each SCK is asynchronous) - Separate data buffers for each interface - Clear data flow path between interfaces
Key Module Structure:
module drum_trigger_top(
// Arduino SPI Interface
input logic arduino_sck,
input logic arduino_sdi,
input logic arduino_cs_n,
// MCU SPI Interface
input logic mcu_sck,
input logic mcu_sdi,
output logic mcu_sdo,
input logic mcu_cs_n,
// Internal data path
// Data flows: arduino_spi_slave → buffer → spi_slave_mcu
);Clock Domain Crossing
Challenge: Handling asynchronous SPI clocks from different masters
Solution: - Use separate clock domains for each SPI interface - Synchronize control signals using synchronizers - Use valid/ready handshaking for data transfer - Avoid mixing clock domains in combinational logic
State Machine Design
Pattern: Clear state machine for SPI transaction handling
States: - IDLE: Waiting for transaction - RECEIVING: Actively receiving data - PROCESSING: Validating and processing packet - READY: Data available for next stage - ERROR: Error condition detected
Benefits: - Clear transaction lifecycle - Easy to debug - Predictable behavior - Error handling built-in
MCU Integration Techniques
SPI Master Configuration
Initialization Sequence: 1. Enable GPIO clocks 2. Configure SPI pins (alternate function) 3. Enable SPI peripheral clock 4. Configure SPI peripheral 5. Enable SPI peripheral
Key Code Pattern:
void SPI_Init(void) {
// 1. Enable clocks
RCC->AHB2ENR |= RCC_AHB2ENR_GPIOBEN;
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
// 2. Configure GPIO pins
GPIOB->MODER &= ~(GPIO_MODER_MODE3 | GPIO_MODER_MODE4 | GPIO_MODER_MODE5);
GPIOB->MODER |= (GPIO_MODER_MODE3_1 | GPIO_MODER_MODE4_1 | GPIO_MODER_MODE5_1);
GPIOB->AFR[0] |= (5 << 12) | (5 << 16) | (5 << 20); // AF5 for SPI1
// 3. Configure SPI
// ... SPI configuration code ...
// 4. Enable SPI
SPI1->CR1 |= SPI_CR1_SPE;
}Reading Data from FPGA
Transaction Pattern:
uint8_t SPI_ReadByte(void) {
// Wait for TX buffer empty
while (!(SPI1->SR & SPI_SR_TXE));
// Write dummy byte to generate clock
SPI1->DR = 0xFF;
// Wait for RX buffer full
while (!(SPI1->SR & SPI_SR_RXNE));
// Read received byte
return SPI1->DR;
}Data Processing
Packet Parsing (from DATA_PIPELINE_VERIFICATION.md): - Header validation: Byte 0 must be 0xAA - Roll: Bytes 1-2 (int16_t, MSB first, scaled by 100 = 0.01 degree resolution) - Pitch: Bytes 3-4 (int16_t, MSB first, scaled by 100) - Yaw: Bytes 5-6 (int16_t, MSB first, scaled by 100) - Gyro X: Bytes 7-8 (int16_t, MSB first, scaled by 2000) - Gyro Y: Bytes 9-10 (int16_t, MSB first, scaled by 2000) - Gyro Z: Bytes 11-12 (int16_t, MSB first, scaled by 2000) - Flags: Byte 13 (bit 0 = Euler valid, bit 1 = Gyro valid) - Reserved: Bytes 14-15 (0x00)
Data Handling: - All values are signed 16-bit integers (int16_t) - Euler angles: Divide by 100 to get degrees - Gyroscope: Divide by 2000 to get actual units (check sensor datasheet for exact scaling) - Check valid flags before using data - Handle endianness correctly (MSB first, big-endian)
Sensor Interface Methods
BNO085 I2C Interface (Arduino Side)
Key Implementation (from ARDUINO_SENSOR_BRIDGE.ino): - Uses Adafruit BNO08x library - I2C communication at 400kHz - Sensor reports at 100Hz (10ms intervals, 10000 microseconds) - Quaternion and gyroscope data from sensor
Configuration:
Adafruit_BNO08x bno08x(BNO08X_RESET);
sh2_SensorValue_t sensorValue;
// Enable rotation vector report (quaternion)
sh2_SensorId_t reportType = SH2_ROTATION_VECTOR;
sh2_ReportInterval_t interval = 10000; // 100Hz (10000 microseconds)
bno08x.enableReport(reportType, interval);
// Enable gyroscope report
reportType = SH2_GYROSCOPE_CALIBRATED;
bno08x.enableReport(reportType, interval);Data Conversion
Quaternion to Euler: - Convert quaternion (w, x, y, z) to Euler angles (roll, pitch, yaw) - Scale by 100 for transmission (0.01 degree resolution) - Handle angle wrapping correctly - Store as signed 16-bit integers
Packet Formatting (16 bytes total): - Byte 0: Header (0xAA) for synchronization - Bytes 1-2: Roll (int16_t, MSB first, scaled by 100) - Bytes 3-4: Pitch (int16_t, MSB first, scaled by 100) - Bytes 5-6: Yaw (int16_t, MSB first, scaled by 100) - Bytes 7-8: Gyro X (int16_t, MSB first, scaled by 2000) - Bytes 9-10: Gyro Y (int16_t, MSB first, scaled by 2000) - Bytes 11-12: Gyro Z (int16_t, MSB first, scaled by 2000) - Byte 13: Flags (bit 0 = Euler valid, bit 1 = Gyro valid) - Bytes 14-15: Reserved (0x00)
SPI Transmission: - CS pin: D10 (FPGA_SPI_CS) - connects to FPGA arduino_cs_n - Clock: D13 (SCK) - Data: D11 (MOSI) - Mode: SPI_MODE0, MSBFIRST, 100kHz
Debugging Approaches
FPGA Debugging
1. Simulation: - Use testbenches for each module - Verify SPI protocol compliance - Check timing constraints - Validate packet parsing
2. Signal Tap / Logic Analyzer: - Monitor SPI signals (SCK, MOSI, MISO, CS) - Verify timing relationships - Check data values - Debug state machine transitions
3. Status LEDs: - Implement LEDs for key states: - Initialized - Data valid - Error condition - Heartbeat
MCU Debugging
1. Serial Output: - Use USART for debug messages - Print received data values - Log SPI transaction status - Monitor timing
2. Breakpoints: - Use debugger to step through code - Inspect register values - Check SPI peripheral status - Verify data buffers
3. Oscilloscope: - Monitor SPI signals - Verify timing - Check CS timing - Validate data transmission
System-Level Debugging
1. End-to-End Testing: - Start with known good sensor data - Verify each stage of pipeline - Check data at each interface - Validate final output
2. Incremental Integration: - Test Arduino → FPGA first - Then test FPGA → MCU - Finally test complete system - Isolate problems to specific stage
Common Pitfalls and Solutions
Pitfall 1: SPI Clock Domain Crossing (CDC) Issues
Problem: Metastability or timing violations when transferring data between asynchronous clock domains (Arduino SCK → FPGA clk → MCU SCK)
Solution (from SENIOR_ENGINEER_AUDIT.md): - Use CS-based safe read approach: wait for CS HIGH (transaction complete) - For SPI Mode 0, CS HIGH guarantees SCK is idle (CPOL=0) - Wait 3 system clock cycles after CS HIGH before reading (provides 10:1 timing margin) - Use atomic reads of complete packets (all 16 bytes in one cycle) - Document timing constraints and CDC strategy - See TIMING_CONSTRAINTS.md for complete analysis
Pitfall 2: CS Timing Errors
Problem: CS asserted/deasserted at wrong times, causing data corruption
Solution: - Ensure CS stays LOW for entire packet - Don’t change CS during active transaction - Add proper delays between transactions - Verify CS timing with oscilloscope
Pitfall 3: Data Format Mismatches
Problem: Data format doesn’t match between stages
Solution: - Document data formats clearly - Use consistent scaling factors - Verify endianness (MSB/LSB order) - Test with known values
Pitfall 4: Buffer Overflow
Problem: New data arrives before old data is read
Solution: - Implement proper buffering - Use valid flags to indicate data availability - Check buffer status before writing - Handle overflow conditions gracefully
Pitfall 5: Power and Ground Issues
Problem: System instability due to power/ground problems
Solution: - Use common ground for all components - Ensure adequate power supply capacity - Add decoupling capacitors - Verify power supply voltage levels
Pitfall 6: SPI Mode Mismatch
Problem: Different SPI modes between components
Solution: - Verify all components use same SPI mode - Check CPOL and CPHA settings - Ensure bit order matches (MSB/LSB) - Verify clock polarity and phase
Additional Resources
Documentation Files
The code repository includes several comprehensive documentation files:
DATA_PIPELINE_VERIFICATION.md: Complete data flow verification showing exact byte-by-byte packet format through all stages (Arduino → FPGA → MCU)ARDUINO_FPGA_CONNECTION_ISSUE.md: Troubleshooting guide for Arduino-FPGA connection issues, CS pin configuration, and SPI settingsTIMING_CONSTRAINTS.md: Complete FPGA timing analysis, clock domain analysis, CDC strategies, and recommended timing constraints for synthesis toolsSENIOR_ENGINEER_AUDIT.md: Comprehensive code review addressing CDC violations, timing issues, and implementation improvements (all critical issues resolved)
Key Learnings
- SPI Protocol: Understanding SPI modes and timing is critical
- Clock Domains: Proper handling of asynchronous clocks prevents many issues
- Data Formats: Consistent data formats across all stages simplifies debugging
- Incremental Testing: Test each stage independently before system integration
- Documentation: Good documentation saves time during debugging
Future Enhancements
Potential improvements for future iterations: - Error detection and correction - Data rate adaptation - Additional sensor support - Real-time filtering - Wireless communication